use crate::ffi::{
    cJSON, cJSON_AddItemToArray, cJSON_AddItemToObject, cJSON_Array, cJSON_CreateArray,
    cJSON_CreateBool, cJSON_CreateNull, cJSON_CreateNumber, cJSON_CreateObject, cJSON_CreateString,
    cJSON_False, cJSON_GetArrayItem, cJSON_GetArraySize, cJSON_GetObjectItem, cJSON_GetStringValue,
    cJSON_Invalid, cJSON_IsArray, cJSON_IsBool, cJSON_IsNull, cJSON_IsNumber, cJSON_IsObject,
    cJSON_IsString, cJSON_NULL, cJSON_Number, cJSON_Object, cJSON_String, cJSON_True,
};
use crate::libc;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use core::ffi::c_char;
use core::ops::Deref;
use core::ptr::NonNull;

pub struct Value(pub(crate) NonNull<cJSON>);

impl Value {
    pub(crate) fn new(v: *mut cJSON) -> Self {
        assert!(!v.is_null());
        Self(NonNull::new(v).unwrap())
    }

    #[inline]
    pub fn is_object(&self) -> bool {
        unsafe {
            if cJSON_IsObject(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    pub fn get(&self, k: *const c_char) -> Option<RefValue<'_>> {
        unsafe {
            let v = cJSON_GetObjectItem(self.0.as_ptr(), k);
            if !v.is_null() {
                Some(RefValue::new(self, Self::new(v)))
            } else {
                None
            }
        }
    }

    pub fn put(&mut self, k: *const c_char, val: Value) {
        unsafe {
            cJSON_AddItemToObject(self.0.as_ptr(), k, val.0.as_ptr());
        }
    }

    pub fn is_array(&self) -> bool {
        unsafe {
            if cJSON_IsArray(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    pub fn get_item(&self, index: usize) -> Option<RefValue<'_>> {
        unsafe {
            let v = cJSON_GetArrayItem(self.0.as_ptr(), index as i32);
            if !v.is_null() {
                Some(RefValue::new(self, Self::new(v)))
            } else {
                None
            }
        }
    }

    pub fn add_item(&mut self, val: Value) {
        unsafe {
            cJSON_AddItemToArray(self.0.as_ptr(), val.0.as_ptr());
        }
    }

    pub fn get_item_num(&self) -> usize {
        assert!(self.is_array());
        unsafe { cJSON_GetArraySize(self.0.as_ptr()) as usize }
    }

    #[inline]
    pub fn is_string(&self) -> bool {
        unsafe {
            if cJSON_IsString(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        unsafe {
            let c = cJSON_GetStringValue(self.0.as_ptr());
            if !c.is_null() {
                let len = libc::strlen(c);
                let s = alloc::str::from_utf8_unchecked(core::slice::from_raw_parts(
                    c as *const u8,
                    len as usize,
                ));
                Some(s)
            } else {
                None
            }
        }
    }

    #[inline]
    pub fn is_number(&self) -> bool {
        unsafe {
            if cJSON_IsNumber(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    #[inline]
    pub fn as_f64(&self) -> Option<f64> {
        unsafe {
            if cJSON_IsNumber(self.0.as_ptr()) != 0 {
                let v = self.0.as_ref().valuedouble;
                if !v.is_nan() {
                    return Some(v);
                }
            }
            None
        }
    }

    #[inline]
    pub fn as_f32(&self) -> Option<f32> {
        unsafe {
            if cJSON_IsNumber(self.0.as_ptr()) != 0 {
                let v = self.0.as_ref().valuedouble;
                if !v.is_nan() {
                    return Some(v as f32);
                }
            }
            None
        }
    }

    #[inline]
    pub fn as_i64(&self) -> Option<i64> {
        unsafe {
            if cJSON_IsNumber(self.0.as_ptr()) != 0 {
                let v = self.0.as_ref().valuedouble;
                if !v.is_nan() {
                    return Some(v as i64);
                }
            }
            None
        }
    }

    #[inline]
    pub fn as_u64(&self) -> Option<u64> {
        unsafe {
            if cJSON_IsNumber(self.0.as_ptr()) != 0 {
                let v = self.0.as_ref().valuedouble;
                if !v.is_nan() {
                    return Some(v as u64);
                }
            }
            None
        }
    }

    #[inline]
    pub fn is_boolean(&self) -> bool {
        unsafe {
            if cJSON_IsBool(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    #[inline]
    pub fn as_bool(&self) -> Option<bool> {
        unsafe {
            let j = self.0.as_ref();
            if (j.type_ as u32 & cJSON_True) != 0 {
                Some(true)
            } else if (j.type_ as u32 & cJSON_False) != 0 {
                Some(false)
            } else {
                None
            }
        }
    }

    #[inline]
    pub fn is_null(&self) -> bool {
        unsafe {
            if cJSON_IsNull(self.0.as_ptr()) == 1 {
                true
            } else {
                false
            }
        }
    }

    #[inline]
    pub fn as_null(&self) -> Option<()> {
        unsafe {
            let j = self.0.as_ref();
            if (j.type_ as u32 & cJSON_NULL) != 0 {
                Some(())
            } else {
                None
            }
        }
    }

    #[inline]
    pub fn new_null() -> Self {
        Self::new(unsafe { cJSON_CreateNull() })
    }

    #[inline]
    pub fn new_bool(b: bool) -> Self {
        Self::new(unsafe { cJSON_CreateBool(if b { 1 } else { 0 }) })
    }

    #[inline]
    pub fn new_object() -> Self {
        Self::new(unsafe { cJSON_CreateObject() })
    }

    #[inline]
    pub fn new_number(v: f64) -> Self {
        Self::new(unsafe { cJSON_CreateNumber(v) })
    }

    #[inline]
    pub fn new_array() -> Self {
        Self::new(unsafe { cJSON_CreateArray() })
    }

    pub fn new_str(s: *const c_char) -> Self {
        Self::new(unsafe { cJSON_CreateString(s) })
    }
}

impl ToString for Value {
    fn to_string(&self) -> String {
        unsafe {
            match self.0.as_ref().type_ as u32 & 0xff {
                cJSON_Number => format!("{}", self.0.as_ref().valuedouble),
                cJSON_String => {
                    let len = libc::strlen(self.0.as_ref().valuestring);
                    let s = alloc::str::from_utf8_unchecked(core::slice::from_raw_parts(
                        self.0.as_ref().valuestring as *const u8,
                        len as usize,
                    ));
                    s.to_owned()
                }
                cJSON_Array => "array".to_owned(),
                cJSON_Object => "object".to_owned(),
                cJSON_Invalid => "invalid".to_owned(),
                cJSON_NULL => "null".to_owned(),
                cJSON_False => "false".to_owned(),
                cJSON_True => "true".to_owned(),
                n => format!("unknown {}", n),
            }
        }
    }
}

pub struct RefValue<'a> {
    parent: &'a Value,
    value: Value,
}

impl<'a> RefValue<'a> {
    pub(crate) fn new(parent: &'a Value, value: Value) -> Self {
        Self { parent, value }
    }
}

impl<'a> Deref for RefValue<'a> {
    type Target = Value;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}
